/*
 * $Id: BoundingBox.java,v 1.2 2007/08/26 18:56:35 gil1 Exp $ $Date: 2007/08/26
 * 18:56:35 $ Copyright (c) Eric Z. Beard, ericzbeard@hotmail.com This library
 * is free software; you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version. This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details. You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package pdf.handler;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * <p>
 * This class simplifies the placement of Strings within a canvas area where the
 * placement of objects is absolute
 * </p>
 * <p>
 * A <code>BoundingBox</code> is just a Rectangle that knows how to find the
 * coordinates for a String based on the desired alignment and
 * <code>FontMetrics</code>. For each new String, a new child
 * <code>BoundingBox</code> is made that can be subtracted from the original box
 * so new Strings can be added
 * </p>
 * <p>
 * One of the more helpful features of this class is the string wrap feature of
 * <code>getStringBounds</code>. The box returned by that method will contain an
 * array of strings that have been broken down to fit the box. The box's
 * coordinates and size will reflect the size of the entire group of strings if
 * it is laid out as expected. Using the returned box and iterating through the
 * array of strings from top to bottom, getting new bounding boxes for each one
 * (with upper left alignment and no padding) will result in the correct string
 * wrap.
 * </p>
 * <p>
 * Note that you will need to have Xvfb running on a Unix server to use this
 * class
 * </p>
 * 
 * @author Eric Z. Beard, ericzbeard@hotmail.com
 * @version $Revision: 1.2 $, $Date: 2007/08/26 18:56:35 $
 */
public class BoundingBox extends Rectangle
{
   /** Used to align a String horizontally in the center of the box */
   public static final int HORIZ_ALIGN_CENTER = 3;

   /** Used to align a String to the left in the box */
   public static final int HORIZ_ALIGN_LEFT = 4;

   /** Used to aling a String to the right in a box */
   public static final int HORIZ_ALIGN_RIGHT = 5;

   private static final int[] HORIZ_ALIGNS = { HORIZ_ALIGN_CENTER,
      HORIZ_ALIGN_LEFT,
      HORIZ_ALIGN_RIGHT };

   /** Percent f line height to space lines */
   public static final int LINE_SPACING_PERCENTAGE = 20;

   /**
    * 
    */
   private static final long serialVersionUID = 1L;

   /** Used to subtract a child from a box, *leaving* the bottom portion */
   public static final int SUBTRACT_FROM_BOTTOM = 7;

   /** Used to subtract a child from a box, *leaving* the left portion */
   public static final int SUBTRACT_FROM_LEFT = 8;

   /** Used to subtract a child from a box, *leaving" the right portion */
   public static final int SUBTRACT_FROM_RIGHT = 9;

   /** Used to subtract a child from a box, *leaving* the top portion */
   public static final int SUBTRACT_FROM_TOP = 6;

   private static final int[] SUBTRACTS = { SUBTRACT_FROM_TOP,
      SUBTRACT_FROM_BOTTOM,
      SUBTRACT_FROM_LEFT,
      SUBTRACT_FROM_RIGHT };

   /** Used to align a String at the bottom of the box */
   public static final int VERT_ALIGN_BOTTOM = 2;

   /** Used to a align a String centered vertically */
   public static final int VERT_ALIGN_CENTER = 0;

   /** Used to align a String at the top of the box */
   public static final int VERT_ALIGN_TOP = 1;

   private static final int[] VERT_ALIGNS = { VERT_ALIGN_CENTER,
      VERT_ALIGN_TOP,
      VERT_ALIGN_BOTTOM };

   /**
    * For testing
    * 
    * @param args
    *            a <code>String[]</code> value
    */
   public static void main(String[] args)
   {
      Point upperLeft = new Point(5, 5);
      Dimension bounds = new Dimension(100, 100);
      BoundingBox parent = new BoundingBox(upperLeft, bounds);
      String string = "Hello World!";
      Font font = new Font("SansSerif", Font.PLAIN, 12);
      Frame frame = new Frame();
      frame.addNotify();
      try
      {
         Image image = frame.createImage(100, 100);
         if (image == null)
            System.err.println("image is null");
         Graphics graphics = image.getGraphics();
         FontMetrics fm = graphics.getFontMetrics(font);
         BoundingBox child = parent
               .getStringBounds(string,
                     BoundingBox.HORIZ_ALIGN_LEFT,
                     BoundingBox.VERT_ALIGN_TOP,
                     fm,
                     5);
         System.out.println("Drawing Point: " +
               child.getDrawingPoint().toString());
         System.out.println("Now testing subtract() method...");

         parent = new BoundingBox(new Point(10, 10), new Dimension(300, 300));
         System.out.println("parent: " + parent.toString());
         child = new BoundingBox(new Point(90, 110), new Dimension(100, 100));
         parent.add(child);
         System.out.println("child: " + child.toString());
         System.out.println();
         System.out.println("subtracting the child from the parent");
         System.out.println("SUBTRACT_FROM_TOP: ");
         parent = parent.subtract(child, SUBTRACT_FROM_TOP);
         System.out.println("new parent: " + parent.toString());
         System.out.println();
         System.out.println("Resetting parent");
         parent = new BoundingBox(new Point(10, 10), new Dimension(300, 300));
         parent.add(child);
         System.out.println("SUBTRACT_FROM_BOTTOM");
         parent.subtract(child, SUBTRACT_FROM_BOTTOM);
         System.out.println("new parent: " + parent.toString());
         System.out.println();
         System.out.println("Resetting parent");
         parent = new BoundingBox(new Point(10, 10), new Dimension(300, 300));
         parent.add(child);
         System.out.println("SUBTRACT_FROM_LEFT");
         parent.subtract(child, SUBTRACT_FROM_LEFT);
         System.out.println("new parent: " + parent.toString());
         System.out.println();
         System.out.println("Resetting parent");
         parent = new BoundingBox(new Point(10, 10), new Dimension(300, 300));
         parent.add(child);
         System.out.println("SUBTRACT_FROM_RIGHT");
         parent.subtract(child, SUBTRACT_FROM_RIGHT);
         System.out.println("new parent: " + parent.toString());
         System.out.println();

         System.exit(0);
      }
      catch (Exception e)
      {
         e.printStackTrace();
         System.exit(1);
      }
   }

   /** The absolute, world location of the box */
   private Point absoluteLocation;

   /** The point to use for Graphics.drawString() */
   private Point drawingPoint;

   /** The string specified in getStringBounds */
   private String fullString;

   /** Link to parent box */
   private BoundingBox parent;

   /**
    * If this box was the result of a getStringBounds call, this array will
    * hold the broken strings
    */
   private String[] stringArray;

   /**
    * Creates a new <code>BoundingBox</code> instance.
    * 
    * @param p
    *            a <code>Point</code>, upper left coords
    * @param d
    *            a <code>Dimension</code>, used to determine height and width
    */
   public BoundingBox(Point p, Dimension d)
   {
      super(p, d);
      this.drawingPoint = this.getLocation();
      this.absoluteLocation = this.getLocation();
   }

   /**
    * Creates a new <code>BoundingBox</code> instance.
    * 
    * @param p
    *            a <code>Point</code> value
    * @param d
    *            a <code>Dimension</code> value
    * @param drawingPoint
    *            a <code>Point</code> value
    */
   private BoundingBox(Point p,
         Dimension d,
         Point drawingPoint,
         Point absolute)
   {
      super(p, d);
      this.drawingPoint = drawingPoint;
      this.absoluteLocation = absolute;
   }

   /**
    * <p>
    * Make the specified box this box's child. Equivalent to
    * <code>child.setParent(parent)</code> where the specified 'parent' is this
    * instance
    * </p>
    * 
    * @param child
    *            a <code>BoundingBox</code>, any box that can fit inside this
    *            one. The results of calling <code>getAbsoluteLocation()</code>
    *            on the child will be altered after this to take into account
    *            the child's new location in the 'world'
    */
   public void add(BoundingBox child)
   {
      child.setParent(this);
   }

   /**
    * <p>
    * Returns false if for any reason this box has negative dimensions
    * </p>
    * 
    * @return true if the box has positive height and width, false otherwise.
    */
   public boolean boxExists()
   {
      return (this.getHeight() > 0 && this.getWidth() > 0);
   } // end boxExists

   /**
    * <p>
    * Checks the horizontal alignment passed into a method to make sure it is
    * one of the valid values
    * </p>
    * 
    * @param hAlign
    *            an <code>int</code> value
    * @return a <code>boolean</code> value
    */
   private boolean checkHAlign(int hAlign)
   {
      int len = HORIZ_ALIGNS.length;
      for (int i = 0; i < len; i++)
         if (hAlign == HORIZ_ALIGNS[i])
            return true;
      return false;
   }

   /**
    * <p>
    * Checks the vertical alignment passed into a method to make sure it is one
    * of the valid values
    * </p>
    * 
    * @param vAlign
    *            an <code>int</code> value
    * @return a <code>boolean</code> value
    */
   private boolean checkVAlign(int vAlign)
   {
      int len = VERT_ALIGNS.length;
      for (int i = 0; i < len; i++)
         if (vAlign == VERT_ALIGNS[i])
            return true;
      return false;
   }

   /**
    * <p>
    * Make a string array from a string, wrapped to fit the box
    * </p>
    * <p>
    * If the line width is too short, the array is just a tokenized version of
    * the string
    * </p>
    * 
    * @param string
    *            - the <code>String</code> to convert to an array
    * @param
    */
   private String[] createStringArray(String string,
         FontMetrics fm,
         int padding,
         int pw)
   {
      if (string == null)
      {
         System.err.println("Tried createStringArray with null String");
         return null;
      }
      if (fm == null)
         System.err.println("Tried createStringArray with null FontMetrics");

      int lw = pw - (padding * 2);

      Vector<String> returnVector = new Vector<String>();
      // Return delimiters as tokens
      StringTokenizer st = new StringTokenizer(string, " \t\n\r\f", true);
      StringBuffer tempBuffer = new StringBuffer();
      StringBuffer finalBuffer = new StringBuffer();

      while (st.hasMoreTokens())
      {
         // Get the next word and add a space after it
         String tempString = st.nextToken();
         tempBuffer.append(tempString);

         // If we haven't reached the width with our current
         // line, keep adding tokens. Also, check for hard returns
         if ((fm.stringWidth(tempBuffer.toString()) < lw)
               &&
               (tempBuffer.toString()
                     .charAt(tempBuffer.toString().length() - 1) != '\n')
                     &&
                     (tempBuffer.toString()
                           .charAt(tempBuffer.toString().length() - 1) != '\r'))
         {
            finalBuffer.append(tempString);
            continue;
         }
         returnVector.addElement(finalBuffer.toString());
         finalBuffer.delete(0, finalBuffer.length());
         tempBuffer.delete(0, tempBuffer.length());
         if ((tempString.charAt(0) != '\n') &&
               (tempString.charAt(0) != '\r'))
         {
            tempBuffer.append(tempString);
            finalBuffer.append(tempString);
         }
         continue;

      } // end while
      returnVector.addElement(finalBuffer.toString());

      int len = returnVector.size();
      // Init the class member field stringArray
      String[] childStrArray = new String[len];
      for (int i = 0; i < len; i++)
      {
         String curStr = returnVector.get(i);
         childStrArray[i] = curStr;
      }

      return childStrArray;

   } // end createStringArray

   /**
    * <p>
    * Take the first line of the string (if it is wrapped, otherwise just take
    * the whole string) and chop the end of it off to make it fit in the box.
    * If the box is smaller than one letter, draw nothing
    * </p>
    * 
    * @param g
    *            the <code>Graphics</code> object to draw to
    * @param fm
    *            the <code>FontMetrics</code> object to use for string sizing
    * @param padding
    *            the <code>int</code> amount of padding around the string
    * @param hAlign
    *            the <code>int</code> horizontal alignment
    */
   public void drawChoppedString(Graphics g,
         FontMetrics fm,
         int padding,
         int hAlign)
   {

      String string = "";
      if (getStringArray() != null)
         string = new String(getStringArray()[0]);
      else
         string = new String(getFullString());
      BoundingBox choppedBox = null;
      try
      {
         choppedBox = getStringBounds(string,
               hAlign,
               VERT_ALIGN_TOP,
               fm,
               padding);
         Point p = choppedBox.getDrawingPoint();
         int x = (int) p.getX();
         int y = (int) p.getY();
         g.drawString(string, x, y);
      }
      catch (StringTooLongException stle)
      {
         // Doesn't fit - start cutting from the end until it does
         StringBuffer buf = new StringBuffer().append(string);
         if (buf.length() == 0)
         {
            System.out
            .println("BoundingBox.drawChoppedString, buf len 0 ??");
            // return;
            throw new RuntimeException();
         }
         buf.deleteCharAt(buf.length() - 1);
         while ((fm.stringWidth(buf.toString()) > (int) getWidth()) &&
               (buf.length() > 0))
            buf.deleteCharAt(buf.length() - 1);

         try
         {
            choppedBox = getStringBounds(buf.toString(),
                  hAlign,
                  VERT_ALIGN_TOP,
                  fm,
                  padding);
            Point pp = choppedBox.getDrawingPoint();
            int xx = (int) pp.getX();
            int yy = (int) pp.getY();
            g.drawString(string, xx, yy);
         }
         catch (StringTooLongException sstle)
         {
            // Must be a really small box!
            sstle.printStackTrace();
         }
      }
   } // end drawChoppedString

   /**
    * <p>
    * This method is called after getting the box by calling
    * <code>getStringBounds</code> on the parent. Wraps the string at word
    * boundaries and draws it to the specified <code>Graphics</code> context.
    * Make sure padding is the same as specified for the
    * <code>getStringBounds</code> call, or you may get an unexpected
    * {@link pdf.handler.StringTooLongException}
    * </p>
    * 
    * @param g
    *            the <code>Graphics</code> object
    * @param fm
    *            the <code>FontMetrics</code> to use for sizing
    * @param padding
    *            an int, the padding around the strings
    * @param hAlign
    *            the <code>int</code> horizontal alignment
    * @throws IllegalArgumentException
    *             if the args are invalid
    * @throws StringTooLongException
    *             if the string won't fit this will only happen if the fm or
    *             padding has been changed since getStringBounds was called
    *             succesfully
    */
   public void drawWrappedString(Graphics g,
         FontMetrics fm,
         int padding,
         int hAlign)
               throws IllegalArgumentException, StringTooLongException
               {
      if (getStringArray() == null)
      {
         Point p = getDrawingPoint();
         int xx = (int) p.getX();
         int yy = (int) p.getY();
         g.drawString(getFullString(), xx, yy);
      }
      else
      {
         int len = stringArray.length;
         for (int i = 0; i < len; i++)
         {
            BoundingBox wrappedBox = null;
            wrappedBox = getStringBounds(stringArray[i],
                  hAlign,
                  BoundingBox.VERT_ALIGN_TOP,
                  fm,
                  0);
            Point pp = wrappedBox.getDrawingPoint();
            int xx = (int) pp.getX();
            if (hAlign == BoundingBox.HORIZ_ALIGN_RIGHT)
               xx -= padding;
            if (hAlign == BoundingBox.HORIZ_ALIGN_LEFT)
               xx += padding;
            int yy = (int) pp.getY() + padding;
            g.drawString(stringArray[i], xx, yy);
            subtract(wrappedBox, BoundingBox.SUBTRACT_FROM_BOTTOM);
         }
      }
               } // end drawWrappedString

               /**
                * <p>
                * Draws lines from the wrapped string until there is no more room and then
                * stops. If there is no string or the box is too small for anything to be
                * drawn, does nothing
                * </p>
                * 
                * @param g
                *            the <code>Graphics</code> object to draw to
                * @param fm
                *            the <code>FontMetrics</code> object to use for string sizing
                * @param padding
                *            the <code>int</code> amount of padding around the string
                * @param hAlign
                *            the <code>int</code> horizontal alignment
                */
   public void drawWrappedStringTruncate(Graphics g,
         FontMetrics fm,
         int padding,
         int hAlign)
   {

      if (getStringArray() == null)
      {
         Point p = getDrawingPoint();
         int xx = (int) p.getX();
         int yy = (int) p.getY();
         if (getFullString() != null)
            g.drawString(getFullString(), xx, yy);
         else
            System.err.println("getStringArray and getFullString are null");
      }
      else
      {
         int totalHeight = 0;
         int len = stringArray.length;
         for (int i = 0; i < len; i++)
         {
            BoundingBox wrappedBox = null;
            try
            {
               wrappedBox = getStringBounds(stringArray[i],
                     hAlign,
                     BoundingBox.VERT_ALIGN_TOP,
                     fm,
                     0,
                     false);
               totalHeight += (int) wrappedBox.getHeight();
               if (getParent() != null)
                  if (totalHeight > (int) (getParent().getHeight()))
                     return;
            }
            catch (StringTooLongException stle)
            {
               stle.printStackTrace();
               return;
            }
            wrappedBox.drawChoppedString(g, fm, padding, hAlign);
            subtract(wrappedBox, BoundingBox.SUBTRACT_FROM_BOTTOM);
         }
      }
   } // end drawWrappedStringTruncate

   /**
    * <p>
    * Get the absolute upper left location point for this box
    * </p>
    * 
    * @return a <code>Point</code> value
    */
   public Point getAbsoluteLocation()
   {
      return absoluteLocation;
   }

   /**
    * <p>
    * Gets the drawing point to use in Graphics drawing methods. After getting
    * a new BoundingBox with getStringBounds(), calling this method will give
    * you an absolute point, accounting for alignment and padding, etc, from
    * which to start drawing the String
    * </p>
    * <p>
    * If getStringBounds was not called (this is a parent box), the upper left
    * coordinates will be returned (this.getLocation())
    * </p>
    * 
    * @return a <code>Point</code>
    */
   public Point getDrawingPoint()
   {
      return drawingPoint;
   }

   /**
    * <p>
    * Returns the full string associated with a call to
    * <code>getStringBounds</code>
    * </p>
    * 
    * @return the full string.
    */
   public String getFullString()
   {
      return fullString;
   }

   /**
    * <p>
    * Get this box's parent box
    * </p>
    * 
    * @return a <code>BoundingBox</code> value
    */
   public BoundingBox getParent()
   {
      return parent;
   }

   /**
    * <p>
    * Get the wrapped strings if this box was from a call to getStringBounds,
    * otherwise this method returns null
    * </p>
    * 
    * @return a <code>String[]</code> array of strings, top to bottom in layout
    */
   public String[] getStringArray()
   {
      return stringArray;
   } // end getStringArray

   /**
    * <p>
    * Gets the location of a String after it is adjusted for alignment within
    * this box. The point's coordinates are either within this box or within
    * the enclosing area.
    * </p>
    * <p>
    * By default, this method enforces string length and throws the exception
    * if it is too long
    * </p>
    * 
    * @param string
    *            a <code>String</code>, the String to be placed
    * @param hAlign
    *            an <code>int</code>, HORIZ_ALIGN_CENTER, HORIZ_ALIGN_LEFT,
    *            HORIX_ALIGN_RIGHT
    * @param vAlign
    *            an <code>int</code>, VERT_ALIGN_CENTER, VERT_ALIGN_TOP,
    *            VERT_ALIGN_BOTTOM
    * @param fm
    *            a <code>FontMetrics</code> object for this String
    * @param padding
    *            an <code>int</code>, the padding around the String
    * @return a <code>Point</code>, the coords to use in drawString()
    * @throws IllegalArgumentException
    *             if the args are invalid
    * @throws StringTooLongException
    *             if the string won't fit
    */
   public BoundingBox getStringBounds(String string,
         int hAlign,
         int vAlign,
         FontMetrics fm,
         int padding)
               throws StringTooLongException, IllegalArgumentException
               {
      return getStringBounds(string, hAlign, vAlign, fm, padding, true);
               } // end getStringBounds (enforce true by default)

   /**
    * <p>
    * Gets the location of a String after it is adjusted for alignment within
    * this box. The point's coordinates are either within this box or within
    * the enclosing area.
    * </p>
    * 
    * @param string
    *            a <code>String</code>, the String to be placed
    * @param hAlign
    *            an <code>int</code>, HORIZ_ALIGN_CENTER, HORIZ_ALIGN_LEFT,
    *            HORIX_ALIGN_RIGHT
    * @param vAlign
    *            an <code>int</code>, VERT_ALIGN_CENTER, VERT_ALIGN_TOP,
    *            VERT_ALIGN_BOTTOM
    * @param fm
    *            a <code>FontMetrics</code> object for this String
    * @param padding
    *            an <code>int</code>, the padding around the String
    * @param enforce
    *            a <code>boolean</code>, if true the method will throw an
    *            exception when the string is too big, if not true it will
    *            break the string down and overrun the bottom of the box. If
    *            the box is too small for even one word, the exception will be
    *            thrown
    * @return a <code>Point</code>, the coords to use in drawString()
    * @see #HORIZ_ALIGN_LEFT
    * @see #HORIZ_ALIGN_CENTER
    * @see #HORIZ_ALIGN_RIGHT
    * @see #VERT_ALIGN_TOP
    * @see #VERT_ALIGN_CENTER
    * @see #VERT_ALIGN_BOTTOM
    * @throws IllegalArgumentException
    *             if the args are invalid
    * @throws StringTooLongException
    *             if the string won't fit and enforce is set to true. The
    *             exception can still be thrown if enforce is false, but only
    *             in cases such as the box having no height or width
    */
   public BoundingBox getStringBounds(String string,
         int hAlign,
         int vAlign,
         FontMetrics fm,
         int padding,
         boolean enforce)
               throws IllegalArgumentException, StringTooLongException
               {
      // Check to make sure the values passed in are valid
      if (!checkHAlign(hAlign))
         throw new IllegalArgumentException("BoundingBox.getStringBounds, " +
               "hAlign invalid : " + hAlign);
      if (!checkVAlign(vAlign))
         throw new IllegalArgumentException("BoundingBox.getStringBounds, " +
               "vAlign invalid : " + hAlign);
      if (fm == null)
         throw new IllegalArgumentException("BoundingBox.getStringBounds, " +
               "FontMetrics null");
      if (string == null)
         throw new IllegalArgumentException("BoundingBox.getStringBounds, " +
               "String null");

      // NOTE: For this portion of the method, parent refers
      // to this object and child refers to the object about
      // to be created. When the absolute point for drawing the
      // String is determined, this object's ancestors are checked.
      Dimension parentSize = this.getSize();

      Point childLocation;
      Dimension childSize;

      // String ascent, width, height, parent, child width, height
      int sa, sw, sh, pw, ph, cw, ch;

      // Child, parent x, y coords for upper left
      int cx, cy, px, py;

      sa = fm.getMaxAscent();
      sw = fm.stringWidth(string);
      sh = sa + fm.getMaxDescent();
      pw = (int) parentSize.getWidth();
      ph = (int) parentSize.getHeight();
      if (pw < 0)
         throw new StringTooLongException(
               "The parent box has a negative width " +
                     " (" + pw + ")");
      if (ph < 0)
         throw new StringTooLongException(
               "The parent box has a negative height" +
                     " (" + ph + ")");
      cw = sw + padding * 2;
      ch = sh + padding * 2;
      px = (int) this.getX();
      py = (int) this.getY();

      String[] childStrArray = null;

      if ((cw > pw) || (string.indexOf("\n") != -1))
      {
         cw = pw - (padding * 2);
         childStrArray = createStringArray(string, fm, padding, pw);
         ch = getWrappedHeight(childStrArray, fm, padding);
         if (ch > ph)
            // If enforce is not true, it means we want the box to
            // be returned anyway (along with the strings in the array)
            // so we can chop them manually and try again
            if (enforce)
               throw new StringTooLongException(
                     "The wrapped strings do not " +
                           "fit into the parent box, pw=" + pw +
                           ", ph=" + ph + ", ch=" + ch + ", cw=" + cw +
                           ", string: " + string);
      }

      // Need to have child width and height, and string array set

      // Child location is relative to this (parent) box, not the world
      if (vAlign == VERT_ALIGN_TOP)
         cy = 0;
      else if (vAlign == VERT_ALIGN_CENTER)
         cy = (ph / 2) - (ch / 2);
      else
         cy = ph - ch;

      if (hAlign == HORIZ_ALIGN_LEFT)
         cx = 0;
      else if (hAlign == HORIZ_ALIGN_CENTER)
         cx = (pw / 2) - (cw / 2);
      else
         cx = pw - cw;

      childLocation = new Point(cx, cy);
      childSize = new Dimension(cw, ch);

      // Drawing location is based on the baseline of the String, and
      // relative to the world, not this box. The drawing point differs
      // from the absolute box location because of padding and ascent
      int dpx, dpy, abx, aby;

      // If this object also has a parent (maybe grandparents), iterate
      // through them and find the absolute 'world' location
      int ancestorTranslateX = 0;
      int ancestorTranslateY = 0;

      BoundingBox ancestor = this;
      while (ancestor.hasParent())
      {
         BoundingBox oldRef = ancestor;
         ancestor = ancestor.getParent();
         // Prevent infinite recursion
         if (ancestor == oldRef)
            break;
         ancestorTranslateX += (int) ancestor.getLocation().getX();
         ancestorTranslateY += (int) ancestor.getLocation().getY();
      }

      // Determine the absolute location for the box
      abx = px + cx + ancestorTranslateX;
      aby = py + cy + ancestorTranslateY;

      // Determine the absolute drawing point for the String
      dpx = abx + padding;
      dpy = aby + padding + sa;

      Point drawingPoint = new Point(dpx, dpy);
      BoundingBox returnChild = new BoundingBox(childLocation,
            childSize,
            drawingPoint,
            new Point(abx, aby));
      this.add(returnChild);
      returnChild.setFullString(string);
      returnChild.setStringArray(childStrArray);
      return returnChild;

               } // end getStringBounds

   /**
    * <p>
    * Get the total height of the box needed to contain the strings in the
    * specified array
    * </p>
    */
   private int getWrappedHeight(String[] strings, FontMetrics fm, int padding)
   {
      int ma = fm.getMaxAscent();
      int md = fm.getMaxDescent();
      int sh = ma + md;
      int hPad = sh / LINE_SPACING_PERCENTAGE;
      sh += hPad;
      int total = sh * strings.length;

      return total + (padding * 2);
   } // end getWrappedHeight

   /**
    * <p>
    * Returns true if this box has a parent. The 'world', or enclosing canvas
    * is not considered a parent
    * </p>
    * 
    * @return a <code>boolean</code> value
    */
   public boolean hasParent()
   {
      return parent != null;
   }

   /**
    * <p>
    * Set the absolute upper left world location point for this box
    * </p>
    * 
    * @param point
    *            a <code>Point</code> value
    */
   public void setAbsoluteLocation(Point point)
   {
      this.absoluteLocation = point;
   }

   // main method is for testing /////////////////

   /**
    * <p>
    * Sets the full string associated with <code>getStringBounds</code>
    * </p>
    * 
    * @param string
    *            a <code>String</code>
    */
   public void setFullString(String string)
   {
      this.fullString = string;
   }

   // Private methods /////////////////////////////

   /**
    * <p>
    * Make the specified box this box's parent
    * </p>
    * 
    * @param parent
    *            a <code>BoundingBox</code> value
    */
   public void setParent(BoundingBox parent)
   {
      // Prevent infinite recursion
      if (this == parent)
         return;
      this.parent = parent;

      // If this box was created empty, without a String inside,
      // determine its absolute location
      if (this.getLocation().equals(this.getAbsoluteLocation()))
      {
         int ancestorTranslateX = 0;
         int ancestorTranslateY = 0;

         BoundingBox ancestor = this;
         while (ancestor.hasParent())
         {
            BoundingBox oldRef = ancestor;
            ancestor = ancestor.getParent();
            // Prevent infinite recursion
            if (ancestor == oldRef)
               break;
            ancestorTranslateX += (int) ancestor.getLocation().getX();
            ancestorTranslateY += (int) ancestor.getLocation().getY();
         }

         this.getAbsoluteLocation().translate(ancestorTranslateX,
               ancestorTranslateY);
      } // end if
   } // end setParent

   /**
    * <p>
    * Set the value of the string array
    * </p>
    * 
    * @param strArray
    *            a <code>String</code> array
    */
   public void setStringArray(String[] strArray)
   {
      this.stringArray = strArray;
   }

   /**
    * <p>
    * Removes the child box from this parent box. The child must have this
    * object as its parent or the method does nothing. The BoundingBox returned
    * will be cut by an area equal to the child area plus the horizontal or
    * vertical strip in which it sits, depending on the 'subtractFrom' value
    * passed in
    * </p>
    * 
    * @param child
    *            a <code>BoundingBox</code> value
    * @param subtractFrom
    *            an <code>int</code>, SUBTRACT_FROM_LEFT, SUBTRACT_FROM_RIGHT,
    *            SUBTRACT_FROM_TOP, SUBTRACT_FROM_BOTTOM
    * @return a <code>BoundingBox</code> value
    * @see #SUBTRACT_FROM_LEFT
    * @see #SUBTRACT_FROM_RIGHT
    * @see #SUBTRACT_FROM_TOP
    * @see #SUBTRACT_FROM_BOTTOM
    */
   public BoundingBox subtract(BoundingBox child, int subtractFrom)
   {
      // First, check to see if the params are valid
      if (child == null)
         throw new IllegalArgumentException("BoundingBox.subtract, "
               + "BoundingBox child is null");
      if (!child.hasParent())
         throw new IllegalArgumentException("BoundingBox.subtract, "
               + "BoundingBox child has no parent");
      if (!(child.getParent() == this))
         throw new IllegalArgumentException("BoundingBox.subtract, "
               + "this is not BoundingBox child's parent");
      // Now that we know the child is this object's child, we continue
      // and check the subtractFrom param
      int len = SUBTRACTS.length;
      boolean valid = false;
      for (int i = 0; i < len; i++)
         if (subtractFrom == SUBTRACTS[i])
            valid = true;
      if (!valid)
         throw new IllegalArgumentException("BoundingBox.subtract, "
               + "subtractFrom invalid: " + subtractFrom);

      // Now we know the child is valid, and if the subtractFrom
      // preference was invalid, we subtract from the bottom

      // The child should no longer be used, since the parent
      // reference will be invalid
      child.setParent(null);

      int cx = (int) child.getLocation().getX();
      int cy = (int) child.getLocation().getY();
      int cw = (int) child.getSize().getWidth();
      int ch = (int) child.getSize().getHeight();
      int px = (int) this.getLocation().getX();
      int py = (int) this.getLocation().getY();
      int pw = (int) this.getSize().getWidth();
      int ph = (int) this.getSize().getHeight();

      switch (subtractFrom)
      {
      case SUBTRACT_FROM_LEFT:
         // This will be useful for right-justified Strings in tables
         pw = cx;
         this.setSize(new Dimension(pw, ph));
         return this;

      case SUBTRACT_FROM_RIGHT:
         // This will be useful for left justified Strings in tables
         px = px + cw + cx;
         pw = pw - cw - cx;
         this.setLocation(new Point(px, py));
         this.setSize(new Dimension(pw, ph));
         return this;

      case SUBTRACT_FROM_BOTTOM:
         py = py + ch + cy;
         ph = ph - ch - cy;
         this.setLocation(new Point(px, py));
         this.setSize(new Dimension(pw, ph));
         return this;

      case SUBTRACT_FROM_TOP:
         ph = cy;
         this.setSize(new Dimension(pw, ph));
         return this;

      default: // Should never happen
      break;
      } // end switch
      return this;
   } // end subtract

} // end class BoundingBox

